<?php

namespace app\logic\es;

use app\common\facade\EsClient;
use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\MissingParameterException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Http\Promise\Promise;
use InvalidArgumentException;
use think\facade\Log;
use Throwable;

abstract class EsBase
{
    /**
     * es实例化的客户端
     * @var Client
     */
    protected Client $client;

    /**
     * 当前操作的索引
     *
     * @var string
     */
    protected string $index;


    public static function getInstance(): static
    {
        return new static();
    }

    /**
     * 构造函数：初始化Elasticsearch客户端
     *
     * 本构造函数在类实例化时自动调用，主要用于初始化Elasticsearch客户端
     * 通过调用EsClient::getClient()方法获取Elasticsearch客户端实例，并将其赋值给类的client属性
     * 这样做是为了在整个类中能够方便地访问Elasticsearch客户端，进行后续的搜索和索引操作
     */
    public function __construct()
    {
        $this->client = EsClient::getClient();
    }

    /**
     * 创建指定索引的文档
     * @param $body
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function index($body): Elasticsearch|Promise
    {
        // 定义创建索引所需参数
        $params = [
            'index' => $this->index,
            'body' => $body
        ];

        // 返回创建索引的响应数据
        return $this->client->index($params);
    }

    /**
     * 删除指定的索引
     *
     * 此函数构造参数数组，通过客户端的indices()方法来删除一个指定名称的索引
     * 如果操作成功，直接返回响应内容；如果过程中捕获到异常，则记录错误日志并重新抛出异常
     *
     * @return Elasticsearch|Promise 删除操作的响应内容，具体结构取决于Elasticsearch客户端的返回值
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function deleteIndex(): Elasticsearch|Promise
    {
        // 构造删除索引所需的参数数组
        $params = [
            'index' => $this->index,
        ];

        // 返回删除操作的响应内容
        return $this->client->indices()->delete($params);
    }

    /**
     * 根据ID从指定索引中获取文档。
     *
     * 此方法使用Elasticsearch客户端根据提供的文档ID获取特定文档。
     * 主要用于基于提供的文档ID获取文档详细信息。
     *
     * @param int|string $id 文档的唯一标识符。
     * @return Elasticsearch|Promise 从Elasticsearch服务器返回的响应，包含检索到的文档信息。
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function get(int|string $id): Elasticsearch|Promise
    {
        // 准备请求所需的参数
        $params = [
            'index' => $this->index,
            'id' => $id
        ];

        // 尝试获取文档
        return $this->client->get($params);
    }


    /**
     * 执行搜索操作
     *
     * 该方法使用提供的匹配条件来搜索索引中的文档它构建了一个查询参数数组，
     * 包含索引信息和查询体，然后调用Elasticsearch客户端的搜索方法如果搜索过程中
     * 出现异常，它会记录错误信息并重新抛出异常
     *
     * @param array $body 搜索条件
     * @return Elasticsearch|Promise 搜索结果，具体类型取决于Elasticsearch客户端返回的数据
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    public function search(array $body): Elasticsearch|Promise
    {
        $params = [
            'index' => $this->index,
            'body' => json_encode($body),
        ];

        // 返回搜索结果
        return $this->client->search($params);
    }


    /**
     * 更新指定文档的信息
     *
     * 该方法用于更新Elasticsearch中指定索引的文档信息它接收一个文档ID和一个包含更新数据的数组，
     * 并尝试更新索引中的文档信息如果在更新过程中遇到任何错误，它会记录错误信息并重新抛出异常
     *
     * @param int|string $id 文档的唯一标识符
     * @param array $doc 包含要更新的字段和对应值的关联数组
     * @param bool $upsert
     * @return Elasticsearch|Promise 更新操作的响应数据，具体结构取决于Elasticsearch客户端的返回值
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function update(int|string $id, array $doc, bool $upsert = true): Elasticsearch|Promise
    {
        // 准备更新操作的参数
        $params = [
            'index' => $this->index,
            'id' => $id,
            'body' => [
                'doc' => $doc,
                'doc_as_upsert' => $upsert, // 如果文档不存在则创建
            ]
        ];

        // 返回更新操作的响应数据
        return $this->client->update($params);
    }

    /**
     * 删除指定ID的文档
     *
     * 本函数通过$id参数构建请求参数数组，调用Elasticsearch客户端的delete方法尝试删除指定ID的文档
     * 如果删除过程中发生异常，将捕获异常并记录到日志中，然后重新抛出异常以便上层处理
     *
     * @param int|string $id 要删除的文档的ID
     *
     * @return Elasticsearch|Promise 删除操作的响应数据
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function delete(int|string $id): Elasticsearch|Promise
    {
        // 构建删除操作所需的参数数组
        $params = [
            'index' => $this->index,
            'id' => $id
        ];

        // 返回删除操作的响应数据
        return $this->client->delete($params);
    }


    /**
     * 批量处理数据到 Elasticsearch
     *
     * @param array $data 要批量处理的数据数组
     * @param int $batchSize 每批处理的文档数量，默认 1000
     * @throws ClientResponseException
     * @throws ServerResponseException
     * @throws InvalidArgumentException
     * @throws Throwable
     */
    public function batches(array $data, int $batchSize = 1000): bool
    {
        // 初始化 bulk 请求参数
        $params = ['body' => []];
        foreach ($data as $k => $row) {
            if (!is_array($row)) {
                throw new InvalidArgumentException("Invalid document data: must be an array.");
            }
            // 添加索引操作和文档 ID 到 bulk 请求
            $params['body'][] = [
                'update' => [
                    '_index' => $this->index,
                    '_id' => $row['id'] ?? null, // 如果没有 ID，Elasticsearch 自动生成
                ]
            ];
            // 添加文档数据到 bulk 请求
            $params['body'][] = [
                'doc' => $row,
                'doc_as_upsert' => true
            ];

            // 每达到批次大小发送 bulk 请求
            if (($k + 1) % $batchSize == 0) {
                $this->executeBulk($params);
                $params = ['body' => []]; // 清空旧的 bulk 请求
            }
        }

        // 如果存在剩余批次则发送
        if (!empty($params['body'])) {
            $this->executeBulk($params);
        }

        return true;
    }

    /**
     * 执行 bulk 请求并处理错误
     *
     * @param array $params
     * @throws ClientResponseException
     * @throws ServerResponseException
     * @throws Throwable
     */
    private function executeBulk(array $params): void
    {

        try {
            $responses = $this->client->bulk($params);
            if (!empty($responses['errors'])) {
                foreach ($responses['items'] as $item) {
                    if (!empty($item['index']['error'])) {
                        $error = $item['index']['error'];
                        $docId = $item['index']['_id'] ?? 'unknown';
                        Log::channel('es')->error("Failed to index document ID {$docId}: " . json_encode($error));
                    }
                }
            }
        } catch (ClientResponseException|ServerResponseException|Throwable $e) {
            Log::channel('es')->error($e->getMessage() . PHP_EOL . $e->getTraceAsString());
            throw $e;
        }
    }


    /**
     * 创建API KEY
     * @param string $name
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    public function apiKey(string $name = "aigf"): Elasticsearch|Promise
    {
        $param['body'] = [
            'name'=>$name,
            'role_descriptors'=>new \stdClass()
        ];
        return $this->client->security()->createApiKey($param);
    }

    /**
     * 分析文本
     * @param string $index
     * @param mixed $body
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    public function analyze(string $index, mixed $body): Elasticsearch|Promise
    {
        $params = [
            'index' => $index,
            'body' => $body
        ];
        return $this->client->indices()->analyze($params);
    }

    /**
     * 插件列表
     * @param array $params
     * @return Elasticsearch|Promise
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    public function plugins(array $params = [])
    {
        return $this->client->cat()->plugins($params);
    }

    /**
     * 检查索引是否存在，如果不存在则创建索引
     *
     * @param array $mappings 索引的 mapping 配置
     * @param array $settings 索引的 settings 配置
     * @throws ClientResponseException
     * @throws MissingParameterException
     * @throws ServerResponseException
     */
    public function ensureIndexExists(array $mappings, array $settings = []): bool|Elasticsearch|Promise
    {
        // 检查索引是否存在

        $exists = $this->client->indices()->exists(['index' => $this->index]);

        if (!$exists->asBool()) {
            // 索引不存在，创建索引
            return $this->client->indices()->create([
                'index' => $this->index,
                'body' => [
                    'settings' => $settings,
                    'mappings' => $mappings,
                ],
            ]);
        } else {
            return false;
        }
    }

    public function indexExists(): bool|Elasticsearch|Promise
    {
        return    $this->client->indices()->exists(['index' => $this->index])->asBool();
    }

    /**
     * 导入数据到指定索引
     *
     * @param string $indexName 索引名称
     * @param array $data 要导入的数据数组，每个元素代表一条记录
     * @return void
     * @throws ClientResponseException
     * @throws ServerResponseException
     */
    public function importData(string $indexName, array $data): void
    {
        $bulkBody = [];
        foreach ($data as $id => $doc) {
            $bulkBody[] = ['index' => ['_index' => $indexName, '_id' => $id]];
            $bulkBody[] = $doc;
        }

        $this->client->bulk(['body' => $bulkBody]);
    }
}